读书笔记 | Effective Objective C 2.0

Effective Objective C 读书笔记整理

第一章 了解OC

第一条

** OC C
关系 C的超集,添加面向对象
语法 消息结构,运行的代码依赖于运行环境 函数调用,运行代码 编辑器(多态:运行时决定,通过虚函数表)
性能 替换运行时组件 重新编译
内存 所有的对象都分配在堆上,不能分配在栈上

关于内存:

与C++不同,OC不允许将OC对象的内存分配到栈(stack)上,只能分配到堆(heap)上。

C++     string str = "123"; 合法
OC      NSString str = @"123";非法
OC      NSString *str = @"123";合法

所以,OC对象需要指针,ARC也只是针对OC对象(堆上的内存)进行管理,分配在栈上的内存,系统自动清理。

第二条

  • 向前声明 @class 的好处:
    • 1、是延迟引入,减少类的使用者所需的引入的头文件数量
    • 2、解决类之间的相互引用
  • #import VS #include : 避免循环引用如果类需要遵从协议,可以在class-continuation分类中)

第三条:多用字面量(语法糖)

  • 优势:
    • 1、简单、易读、防Nil;特别是NSArray、NSDictionary生成时遇到nil会报错,可以提前排查出问题
  • 局限:
    • 1、生成不变量,如果需要可变量,需要mutbleCopy;
    • 2、也紧紧局限Foudation框架

#define VS 类型变量

  • #define 预处理,直接替换,不包含类型变量
  • 类型变量:包含类型
    • static : 作用域只在当前编译单元(.M)
    • const : 不可变
    • 在全局变量中,不添加static, 编译器会自动添加外部符号(extern symbol),如果其他文件中也这样定义,会出现重定义,如果需要不同文件中公用变量,需要在.m中进行定义,在.h中添加extern

第五条:枚举

  • 1、用枚举表示状态、选项、状态码
  • 2、枚举值可能同时使用,定义为2的幂,可位或 组合使用
  • 3、NS_ENUM and NS_OPTIONS (宏定义,根据不同模式,选择不同方式) 指明类型,不会采用编译器默认的类型,起到一定保护作用
  • 4、枚举类型 switch 不添加default,这样添加新枚举类型时,会提示错误。

    只是会未枚举的类型,但是也需要进行Default

第二章 对象、消息、运行时

第六条:

  • 实例变量:_someProperty,可通过偏移量访问数据
  • 属性:实例变量+存取方法+属性特质:通过点语法可以读取设置属性;

    • 属性会自动生成_property的实例变量,可以通过@synthesize 设置实例变量别名,@dymanic则不自动生成实例变量+存取方法,编译器不会报错,会在运行时找到相关的变量和方法(NSManagerObject)
    • 属性可以分为4类:原子性、读写权限、内存管理语义、方法名

      其中需要特别指出的是copy
      在定义NSString、NSArray、NSDictionary等支持copy协议,且存在可变类型(比如NSMutableString),属性需要设置为copy,防止一个NSMutableString赋值给属性时,属性就为可修改的
      同样NSMutableArray,在设置属性时,需要设置为strong,考虑copy的语义

    • 在初始化时:1、copy的属性,建议在初始化时就copy形参 2、在init方法中不建议用存取方法

      - (instancetype)initWithname:(NSString *)name
      {
      self = [super init];
      if (self) {
          _name = [name copy]
      }
      return self;
      }
      

ios开发atomic不能保证真正的原子,如果需要锁,需要更深层的锁定机制,出于性能一般都用nonatomatic.MAC OS X 则无此瓶颈.

第7条: 直接访问变量 VS 存取方法

  • 直接访问
    • 1、直接访问速度更快,无需方法派发
    • 2、直接访问,不会调用设置方法,copy属性,不会拷贝属性,而且保留新值释放旧值
    • 3、不能KVO
    • 4、不便于调试
  • 存取方法:
    • 1、init中不要用存取方法,防止子类覆盖
    • 2、惰性初始化一定要用存取方法

第8条

== 判断指针是否相等, isEqualTo判断类型、属性和hash值 (isEqual会根据类,进行方法分发,工厂方法),在复写isEqual方法时,需要注意其他情况调用super
关于hash值:如果collection类型的属性,直接写死固定值,会造成该固定值的对应的value变多,而影响性能。如果通过整体求hash,也出现中间变量,存在性能损耗。可以多每个属性求hash,在进行与或处理

注意:把对象放入collection之后,改变其内容会造成很严重的后果

{1, 2} -> NSSet *set // {{1, 2}}
{1} -> NSMutableArray *array
array - > set // {{1}, {1, 2}}
{2} -> array
set //{{1,2}, {1, 2}}
set -> NSSet NewSet //{{1, 2}}

第9条 类族

Cocoa里面很多类族实现,这种工厂方式的实现,因此不能用[subA class] == [A class]的方式进行判断,应该使用类型查询方式isKindOfClass进行类型判断。

第10条 关联对象

objc_setAssociateObject objc_getAssociateObject objc_removeAssociateObject

与Dictionary比较 设置关联对象的key一般是“不透明指针”,所以用静态全局变量作为key;同时要指定内存管理语义,用于模仿拥有 和 非拥有关系

第11条 objc_msgSend

  • 消息由接受者、选择子、参数组成,给对象发送消息,相当于对象调用方法

  • 每个类都有一张函数调用表,key为选择子,value为实际调用的函数值。尾调用优化技术,使跳转更加简单:直接跳转,不需要调用堆栈,进行优化。

第12条 消息转发

upload successful

对象将无法解读的选择子交给其他对象处理,可以模拟多重集成的

第13条 method swizzing

upload successful

第14条 理解类对象

继承是通过super_class, 元类是通过isa

upload successful

类对象是单例,可以用==,来判断内存是否相等

upload successful

第三章 接口和API设计

第15条:用前缀避免命名空间冲突

第16条:

提供“全能初始化方法”:designated initializer OR Initializer from NSCoding;子类与超类不同,子类需要覆盖,超类需要在方法中写Assert

第17条 实现description && debugDescription

第18条 尽量使用不可变对象

  • 1、.h 中readonly, .m中readwrite,

    但是在对象外面还可以通过KVC的方式(setValue forKey)进行更改(hack);更加brutal 是通过类型查询信息找到对应实例变量在内存中的偏移量,从而进行设置

  • 2、可变的collection,应该通过相关方法,修改可变对象

第19条 使用清晰而协调的命名方式

第20条

为私有方法添加前缀,但是不要单用一个下划线,这是预留苹果公司用的

第21条 理解OC的错误类型

ARC不是异常安全的,抛出异常,未释放的对象不能自动释放。如果想“异常安全”,增加-fobc-arc-exception标志;

严重错误,抛出NSException;不严重用nil,0、NSError

第22条 理解NSCoping

—copyWithZone:(NSZone *)zone

复制对象一般进行浅拷贝,深拷贝可单独写一个方法。深拷贝会将底层数据一起拷贝,包括实例变量。

第四章 协议与分类

分类是建立在OC运行时基础上的;协议一般用于委托模式

第23条 通过委托与数据源进行对象间的通讯

  • 1、把需要处理的事件方法定义成协议
  • 2、对象从另外一个对象获取数据时,定义成数据源协议
  • 3、若有必要,可实现含有位段的结构体,将委托对象是否响应相关协议缓存其中,(直接在setDelegate方法中进行缓存)

第24条 通过分类方法 将类的实现代码分散到便于管理的多个分类中

  • 1、划分成不同的功能区
  • 2、调试方便,因为分类名会出现在类名后面;
  • 3、私有的可以考虑private分类

第25条 为第三方分类及方法添加前缀

如果二个分类提供的方法重名,后编译分类方法会覆盖前面分类方法,分类编译顺序与添加到工程中的顺序有关;如果方法名相同,分类会覆盖苹果自带的方法

第26条 不要在分类中申明属性

不要再分类中声明属性(class-continuation除外),虽然技术上可行。如果声明,会出现warning,原因是分类中无法合成与声明属性相关的变量,所以需要在分类中实现存取方法,并且实现中声明为@dynamic,意思就运行时在提供。当然关联对象也可以实现这种需求,但是仍然建议只在分类中提供方法

第27条 使用class-continuation隐藏实现细节

为什么要有这种分类:因为可以定义方法和实例变量。

隐藏实例方法和方法,也可以避免不必要头文件的引入,特别是对于OC++而言,引入的C++头文件。这样.h中进行向前声明,避免引入不必要的头文件,
也可以将类遵循的协议放在class-continuation,但是向前声明delegate却会有警告,因为引入.h文件,编译器看不到协议的定义及包含的方法。

为什么可以定义方法和实例变量:因为ABI机制,我们无须知道对象大小也可以使用。

upload successful

第28条 通过协议提供匿名对象

如果具体类型不重要,只是能响应特定方法,那么可使用匿名对象表示,声明为id类型,来隐藏类型名称

第五章 内存管理

第29条

1、对象创建出来引用计数至少为1,因为在alloc 或者init方法中,其他对象对其进行持有。(思考下面autorelease)

2、引用计数的跟对象是NSApplication 或者 UIApplication,都是在main函数中。

3、set方法中MRC的顺序 保留新的值,释放旧的值,在赋值。

-(void)setFoo:(id)foo{
[foo retain];
[_foo release];
_foo = foo;
}
//这种情况下,如果foo 和 _foo是同一个值,就会出现问题
-(void)setFoo:(id)foo{
[_foo release];
_foo = [foo retain];
}

4、autorelease 延长对象生命期,在跨越方式调用便捷后依然存活一段时间。即会在稍后将引用计数减1,通常是下一个event loop,也可能更早(自动释放池会被释放)。这样就方便了函数调用返回对象不会被立即release或者无法release,导致内存泄露。

第30条

ARC只负责OC对象的内管管理,CoreFoundation 对象 不归 ARC 管理,要进行手动CFRetian/CFRelease

ARC在调用下面方法(retain,release,autorelease,dealloc)是非法的,并且它并不是通过OC消息转发机制,而是直接通过底层C语言,性能更好,而且因为保留释放比较频繁,可以对其进行抵消,节省CPU周期。比如objc_autorrelaseReturnValue + retain = objc_RetainautoreleaseReturnValue;这一过程是可通过标志位完成

ARC命名规则:
alloc、new、copy、mutablecopy 生成的对象,要负责释放对象,而其他方法则不需要,会在方法最后添加auotorelease在稍微释放。

变量的内存管理语义:
ARC :_objc = [SomeClass New];
MRC:id tmp = [SomeClass New]; _objc = [tmp retian]; [tmp release];

ARC如何清理实例变量:通过OC++的cleanup routine,调用回收对象的析构函数.cxx_destruct方法,并且自动调用超类的dealloc方法

第31条 在dealloc中只释放非OC对象引用并解除监听

运行时会在适当的时候调用dealloc,不要主动调用dealloc,但是手动 需要最后调用 super dealloc

1、开销较大或者系统内资源(比如文件描述符、套接字、大块内存等)不在dealloc中进行,应该单独提供方法进行释放,还有一个原因是系统并不保证每个创建出来的对象dealloc都会执行,也可以考虑在Appdelegate中种植方法执行清理,防止内存泄露

2、在dealloc不建议调用其他函数,防止调用过程中对象已经销毁。也不要调用属性的存取方法,因为有人会对其进行覆盖,也可能处于KVO下。(这个存取方法待讨论)

3、self.tableView.delegate 设置为nil

特别需要注意的是:
iOS 8 下tableView的 delegate 和 dataSource 是 assign的,如果不设置为nil会发生野指针crash

第32条 编写异常安全的代码,留意内存管理问题

MRC时,可以将内存释放写在finnal里面(提问:为什么不能写在try 和 catch 里面),但是变量就必须放在块的外面

ARC时:不会自动处理try catch的内存管理,因为ARC不能调用release,所以需要很多样板代码,进行跟踪清理对象,影响运行时性能。因为ios认为因为异常而终止程序,内存管理也就没有必要了。

1、通过-fobjc-arc-exceptions进行开启安全异常处理,默认情况是关闭的。OC++模式是默认开启的

2、建议通过NSError方式进行错误捕捉。

第33条 以弱引用避免保留环

垃圾回收(Garbage collector)会检查引用环,并且会将所有的引用对象都回收。但是ios从未支持过该功能

ARC weak引用会自动清理,由运行时系统来实现

第34条

以自动释放块 降低内存峰值。特别是读取大块数据时,比如图像,数据库。

1、自动释放池存放在栈上,对象收到autorealease方法后,系统将其放在最顶端的池里

2、推荐@autoreleasepool,NSAutorelasePool 在MRC时,需要drain来进行释放,而且“比较重”

第35条 僵尸对象调试 内存管理问题

当对象已经释放,但是还没被覆盖时,调用这块内存会正常工作,但是存在很大风险。Cocoa:系统在回收对象时,可以不真的将其回收,而是把它转化为僵尸对象。NSZombieEnabled设置为yes,或者在scheme中勾选。

僵尸类是从NSZombie模板复制出来的,并且可以保留原类名字,不采用继承的方式,是因为效率因素的考虑。

upload successful

其实现原理是:
修改对象的isa指针,让它指向僵尸类,使对象变成僵尸对象,僵尸类能影响所有的选择子:打印相关消息,并且终止程序。

创建新类,并且转化为僵尸对象:

upload successful

在消息转发机制中,forwarding相应所有的选择子

upload successful

第36条 不要用retainCount,ARC后正式废弃

while ([objc retianCount]) {
    [objc release]
} 

1、objc可能会在后续自动释放,在此释放会crash

2、retainCount 可能永远不为0,系统优化释放行为,为1是就回收了。没有1到0的过程。

第六章 块与大中枢派发

第37条:理解“块”

  • 1、块:只要有支持次特性的编译器以及能执行块的运行期组件,就可以在C、C++、OC、OC++中使用。(待理解???

  • 2、块定义,参考

    局部变量:

    1
    returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

>
属性:
>

1
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);

>
方法形参:
>

1
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;

实参:

1
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

>
typedef
>

1
2
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

  • 3、关于捕获外部变量:运行块所需的全部信息都可以在编译期确定。而且是捕获栈上的变量,如果要修改栈上的变量,需要声明时,加__block

如下,对象存在堆上,所以不需要加__block

1
2
3
4
5
6
NSMutableArray *mutablArray = [NSMutableArray array];
void (^block)() = ^{
[mutablArray addObject:@"1"]; // 1 OK
// mutablArray = [NSMutableArray array]; // 2 Error
};
block();

注意:需要注意的是,在block内部中,_someInstance实际是self->_someInstance,因此也会捕获self

  • 4、块的内部结构

upload successful
invoke 是指向函数实现

descriptor指向结构体:copy 、 dispose分别对应引用计数的+1和-1

  • 5、栈、堆、全局块
    定义block的时候,所占内存都是在栈上的。(只是一般而言,如果定义成实例变量,那么就在堆上了)

upload successful
VS
upload successful
自己尝试一下输出结果

定义的二个块只在if else 作用域内起作用,通过copy 复制到堆上,就成了带引用计数的对象了。可以在定义范围之外的地方使用。

全局块:不捕捉任何对象,运行时是无状态的。全局块的拷贝是个空操作,因为全局块不会被系统回收,相当于单例。这样的处理只是技术上的优化。

1
2
3
void (^block)() = ^{
NSLog(@"This is the block");
};

第38条:为块创建typedef

1
return_type (^block_name)(parameters)

优点:

  • 1、定义变量一样定义block
  • 2、重构时如果给Block多增加参数,那么只需修改相应的块签名(typedef),其他引用block的地方也会自动报错,避免遗漏

第39条 用handler块降低代码分散程度

  • 1、使用delegate 会使代码结构过于分散,可以直接只用回调块,使块和相关对象放在一起,避免通过delegate透传数据
  • 2、通过handler,增加队列参数,决定放在哪个队列上。
  • 3、Error块和Succes块 放在一起,可以处理返回结果中数据异常的情况 VS Error 和 Success分开处理,更加清晰。个人更加意向前者

第40条 用块引用其所属对象避免出现保留环

  • 1、设计API时,可以考虑在调用完complete的块之后,将环中的某个对象设置为nil,解除环,避免API调用者没有处理保留环的问题。也可以通过weakify的方法。
  • 2、API调用者可以通过weakify的方法,解除保留环的问题;

第41条 多用派发队列,少用同步锁

  • 1、派发队列更加单实现同步语义。
    GCD之前,有2种方法:

    • 1、同步块 @synchrnoized(someObject)一般是对self创建锁,但是其中会涉及与self无关的代码,降低代码效率。
    • 2、NSLock对象,通过[_lock lock] [lock unlock]加锁,解锁;NSRecuresiveLock 递归锁
      同一个锁的同步块,顺序执行

    通过atomic 属性同步,这是通过synchrnoized的方式实现的?

  • 2、同步和异步派发结合可以实现加锁机制一样的同步问题,但是却不阻塞异步派发的进程,但是仍然无法正确同步
  • 3、使用同步队列及栅栏块可以令同步行为更加高效。
  • 4、异步派发,需要拷贝块,因此异步派发不一定会比同步快,需要考虑拷贝块与执行块的时间

第42条 多用GCD,少用performSelector系列方法

upload successful

  • 1、会发生warning,导致内存泄漏,因为编译器不知道调用什么选择子,方法签名、返回值,无法通过ARC对返回值进行管理。
  • 2、选择子太过局限,返回类型(void或者id,不能是struct)和参数都有局限。
  • 3、如果把任务放在指定线程执行,用GCD和块,毕竟块可以捕获外部变量。

第43条 GCD VS 操作队列(NSOperation)

upload successful

NSOperation 好处:

  • 1、取消操作
  • 2、指定依赖关系
  • 3、通过KVO监控NSoperation对象的属性
  • 4、指定操作的优先级
  • 5、可以复用NSOperation对象

NSNotifationCenter 就是使用的操作队列

第44条 使用dispatch group进行任务分组

  • 1、dispatch_group_async 包含block,用于回调
  • 2、dispatch_group_enter && dispatch_group_leave

dispatch_group_wait (阻塞)使用表示group可以阻塞的时间;

dispatch_group_notify(不阻塞),使用group结束的回调。

dispatch_apply 用于重复执行的次数

第45条 dispatch_once 只执行一次、线程安全

1、之前通过@synchronized(self) 创建单例,比dispatch-once慢二倍

2、需要一个标记,标记声明为static 或者global,目的是标记都相同

第46条 不要使用dispatch_get_current_queue

1、dispatch_get_current_queue 已经废弃,只做调试用。最主要是也不准确,如下:

upload successful

2、派发队列是按照层次组织的,无法单用某个队列对象来描述当前队列这一概念(如上)

3、dispatch_get_current_queue可以解决不可重入代码引起的思索,一般用“队列特定数据”来解决

1
2
3
4
dispatch_queue_set_specific(dispatch_queue_t queue, const void *key,
void *_Nullable context, dispatch_function_t _Nullable destructor);
void *_Nullable dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);

用法具体见说明文档(补充链接)

与NSdictionary不同,更像 关联引用,值也是不透明的void指针,ARC很难进行管理,因此最后一个参数是析构函数

upload successful

第七章 系统框架

第47条:熟悉系统框架

Foundation OC语言:(CoreFoundation : C语言)

upload successful

第48条 多用块枚举,少用for循环,

  • 1、枚举方式:
    1.1、for循环 1.2、NSEnumerator 遍历 1.3、快速遍历、块枚举
  • 2、块枚举,支持GCD来并发执行遍历操作
    1
    2
    3
    4
    typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {
    NSEnumerationConcurrent = (1UL << 0),
    NSEnumerationReverse = (1UL << 1),
    };

如果提前知道collection对象类型,应修改块签名,指出对象具体的类型

第49条:

upload successful

第50条:

upload successful

第51条:

upload successful

第52条:

upload successful